/*
 * TMap Designer - A Thematic Map design software
 * Copyright (C) 2006 World Health Organization
 * http://www.who.int
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package who.tmap.toolkit.impl;

import java.awt.Color;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.GraphicsConfiguration;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.Transferable;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.awt.image.BufferedImage;
import java.awt.image.VolatileImage;
import java.awt.print.PageFormat;
import java.awt.print.Printable;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;
import javax.swing.JScrollPane;
import javax.swing.JViewport;
import javax.swing.RepaintManager;

import org.apache.log4j.Logger;

import who.tmap.bo.BaseMap;
import who.tmap.bo.ThematicMap;
import who.tmap.bo.cd.CartographicDisplay;
import who.tmap.bo.cd.point.CDPoint;
import who.tmap.common.TMapException;
import who.tmap.toolkit.EditMode;
import who.tmap.toolkit.MappingToolKit;
import who.tmap.toolkit.shape.MapShape;
import who.tmap.toolkit.shape.MapShapes;
import who.tmap.toolkit.shape.PolygonShape;
import who.tmap.toolkit.shape.PolygonShapeDisputedBorder;
import who.tmap.toolkit.shape.annotation.AnnotationShape;
import who.tmap.toolkit.shape.annotation.BackgroundImageShape;
import who.tmap.toolkit.shape.annotation.LegendShape;
import who.tmap.toolkit.shape.annotation.TitleShape;

import com.sun.image.codec.jpeg.ImageFormatException;

/**
 * Takes care of all the mapping tasks in the application
 * @author stl
 */
public class MappingToolKitImpl implements MappingToolKit, ClipboardOwner {

    private static Logger logger = Logger.getLogger(MappingToolKitImpl.class);
    public static final String VERSION ="1.2.2";
    
    private static final Color DEFAULT_COLOR =Color.BLACK;
    private static final Color DEFAULT_BACKGROUND_COLOR =Color.WHITE;
    
    private Container container;
	private Point2D anchor, drag;
	private MapShape editedShape;
	private BackgroundImageShape bis;
	private String currentLayer;
	private Color currentColor, backgroundColor;
	private TitleShape titleShape;
	private LegendShape legendShape;

	private List<MapShape> shapes =null;
	private List<MapShape> selectedShapes =null;
	
	private boolean baseMapVisible =true;
	private boolean cartographicDisplayVisible =true;
	private boolean annotationsVisible =true;
	
	private ThematicMap thematicMap =null;
	
	private MappingToolKitPaintHelper mtkPaintHelper =new MappingToolKitPaintHelper( this );
	private MappingToolKitShapeHelper mtkShapeHelper =new MappingToolKitShapeHelper( this );
	private MappingToolKitScalingHelper mtkScalingHelper =new MappingToolKitScalingHelper( this );	
	private MappingToolKitBitmapHelper mtkBitmapHelper =new MappingToolKitBitmapHelper( this );
	private AttrHelper attrHelper =new AttrHelper();
	
	private int editMode;
	private int dbaseJoinIdx;
	
	private BufferedImage selectedShapesImg;
	private VolatileImage volatileImage;
	
	private Boolean repaintNeeded =true;
	
	MappingToolKitImpl() {
		init();
	}
	
	private void init() {
		anchor =null; 
		drag =null;
		editedShape =null;
		currentLayer =null;
		currentColor =DEFAULT_COLOR;
		backgroundColor =thematicMap != null && thematicMap.getAnnotations() != null ? thematicMap.getAnnotations().getBackgroundColor()
																					 : DEFAULT_BACKGROUND_COLOR;
		mtkScalingHelper.init();
		
		baseMapVisible =true;
		cartographicDisplayVisible =true;
		annotationsVisible =true;
		
		thematicMap =null;
		
		shapes =new ArrayList<MapShape>();
		try {
			bis =(BackgroundImageShape)addShape( MapShapes.BACKGROUND_IMAGE );
			titleShape =(TitleShape)addShape( MapShapes.TITLE );
			legendShape =(LegendShape)addShape( MapShapes.LEGEND );

		} catch (TMapException e) {
			logger.error(e);
		}
		selectedShapes =new ArrayList<MapShape>();	
		
		editMode =EditMode.NONE;
		
		dbaseJoinIdx =-1;
		
		repaintNeeded =true;
		setSelectedShapesHidden( false );
	}
	
	/**
	 * Reset mapping tool kit
	 */
	public void reset() {
		init();
		
		mtkScalingHelper.revalidateScrollPane( container );
		
		container.repaint();
	}
	
	// For test purpose only
	public boolean isRepaintNeeded() {
		return repaintNeeded;
	}
	

	public void setRepaintNeeded(boolean repaintNeeded) {
		this.repaintNeeded = repaintNeeded;
	}
	

	/**
	 * Display a line while building a line or a polygon segment
	 */
	public void xorLine( Graphics g, Point2D p1, Point2D p2 ) {
		
	    if (g == null) {
			return; 
		} 
		
		g.setColor(Color.black);
		g.setXORMode(Color.white);
		
		g.drawLine( (int)p1.getX() , (int)p1.getY(), (int)p2.getX(), (int)p2.getY() );
					
		g.dispose();
	}	
	
	/**
	 * Display a rectangle for multi shape selection
	 */
	public void xorRectangle( Graphics g, Point2D p1, Point2D p2  ) {
	
		if (g == null) {
			return; 
		}
		
		g.setColor(Color.black);
		g.setXORMode(Color.white);

		Rectangle2D rect =new Rectangle2D.Double();
		rect.setFrameFromDiagonal( p1, p2 );

		g.drawRect( (int)rect.getX(), (int)rect.getY(), (int)rect.getWidth(), (int)rect.getHeight() );

		g.dispose();
	}
	
	/**
	 * Display an ellipse while building an ellipse
	 */
	public void xorEllipse( Graphics g, Point2D p1, Point2D p2  ) {
	
		if (g == null) {
			return; 
		}
		
		g.setColor(Color.black);
		g.setXORMode(Color.white);

		Rectangle2D rect =new Rectangle2D.Double();
		rect.setFrameFromDiagonal( p1, p2 );

		((Graphics2D)g).drawOval( (int)rect.getX(), (int)rect.getY(), 
								  (int)rect.getWidth(), (int)rect.getHeight() );

		g.dispose();
	}	

	/**
	 * Use anti-aliasing so it looks good especially when zooming
	 * @param g
	 */
	private void setRenderingHint( Graphics2D g ) {
		g.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
		g.setRenderingHint( RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON );
        g.setRenderingHint( RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE );
 	}
	
	/**
	 * Apply map translation
	 * @param g
	 */
	private void translate( Graphics2D g, PageFormat pf ) {
		g.translate( -mtkScalingHelper.getMinX() 
						+ ( pf == null ? 0 : ( pf.getImageableX() * mtkScalingHelper.getScreenToMapFactor() ) )
				   , mtkScalingHelper.getMaxY() 
						+ ( pf == null ? 0 : ( pf.getImageableY() * mtkScalingHelper.getScreenToMapFactor() ) ) );
	}
	
	public double processMapFactor() {
		return mtkScalingHelper.processMapFactor( container );
	}
	
	public double processMapFactor( double width, double height ) {
		return mtkScalingHelper.processMapFactor(width, height);
	}
	
	public double getMapFactor() {
	    return mtkScalingHelper.getMapFactor();
	}

	public void paintThematicMap( Graphics2D g ) {
		paintThematicMap( g, null, null );
	}
	
	/**
	 * Generate title automatically based on map name, 
	 * data column name and system date 
	 *
	 */
	public void updateTitle() {
		if ( !thematicMap.getAnnotations().isTitleEditedByUser() ) {
			String baseMapName ="";
			if ( thematicMap.getBaseMap() != null && thematicMap.getBaseMap().getFile() != null ) {
				File baseMapFile =thematicMap.getBaseMap().getFile();
			
				if ( baseMapFile != null ) {
					// remove directory info
					if ( baseMapFile.getParent().length() > 0 ) {
						baseMapName =baseMapFile.getAbsolutePath().substring( baseMapFile.getParent().length() +1 );
					} else {
						baseMapName =baseMapFile.getAbsolutePath();
					}
					
					int pos =baseMapName.lastIndexOf( '.' );
					// remove extension
					if ( pos != -1 ) {
						baseMapName =baseMapName.substring( 0, pos );
					}
					
					// Initial upper case + the rest of the name lower case
					if ( baseMapName.length() > 1 ) {
						baseMapName =baseMapName.substring(0, 1).toUpperCase() + baseMapName.substring(1).toLowerCase();
					}
				}	
				
			}				   
			
			CartographicDisplay cd =thematicMap.getCartographicDisplay();
			String dataColumnName ="";
			if ( !(cd instanceof CDPoint) ) {
				dataColumnName =cd.getDataColumnName();
				
				// Initial upper case + the rest of the name lower case
				if ( dataColumnName != null && dataColumnName.length() > 1 ) {
					dataColumnName =dataColumnName.substring(0, 1).toUpperCase() + dataColumnName.substring(1).toLowerCase();
				} else {
					dataColumnName ="";
				}
			}
			
			StringBuilder title =new StringBuilder( baseMapName );
			if ( !"".equals( baseMapName ) && !"".equals( dataColumnName ) ) {
				title.append( ", " );
			}
			
			title.append( dataColumnName )
				 .append( "\n" )
				 .append( DateFormat.getDateInstance( DateFormat.MEDIUM ).format( new Date() ) );
			
			thematicMap.getAnnotations().setTitle( title.toString() );
			
		}
	}
	
	public void captureSelectedShapes() {
		JViewport viewPort = ((JScrollPane)container.getParent().getParent()).getViewport();
		
		Rectangle viewRect =viewPort.getBounds();
		
		BufferedImage viewImg = new BufferedImage( viewRect.width * 2, viewRect.height * 2, BufferedImage.TYPE_INT_ARGB );
	    Graphics2D g = viewImg.createGraphics();
	    
	    g.setBackground( thematicMap.getAnnotations().getBackgroundColor() );
	    g.setColor( thematicMap.getAnnotations().getBackgroundColor() );	    

	    Rectangle fillRect =new Rectangle( new Dimension( viewImg.getWidth(), viewImg.getHeight() ) );

	    g.draw( fillRect );
	    //g.fill( fillRect );
	    
	    g.setColor( Color.BLACK );	
	    
		setRenderingHint( g );
		
		mtkScalingHelper.scale( g );
		
		g.translate( -mtkScalingHelper.getMinX() 
						- ( mtkScalingHelper.getScreenToMapFactor() * viewPort.getViewPosition().x ) 
				   , mtkScalingHelper.getMaxY() 
						- ( mtkScalingHelper.getScreenToMapFactor() * viewPort.getViewPosition().y ) ); 
		
		mtkScalingHelper.setStrokeWidth( g );
		
		Rectangle2D selectedShapesBounds =null;
		
		for( MapShape shape : getSelectedShapes() ) {
			shape.setZoomFactor( getZoomFactor() );
			shape.setMapFactor( getMapFactor() );
			if ( shape instanceof PolygonShape || shape instanceof PolygonShapeDisputedBorder ) {
				shape.setColor( getThematicMap().getAnnotations().getBaseMapColor() );
			}
			
			shape.select( false );
			shape.draw( g );
			shape.select( true );
			
			if ( selectedShapesBounds == null ) {
				selectedShapesBounds =(Rectangle2D)shape.getBounds().clone();
			} else {
				selectedShapesBounds.add( (Rectangle2D)shape.getBounds() );
			} 

		}
		
		int x =(int)Math.abs( Math.abs( selectedShapesBounds.getMinX() -mtkScalingHelper.getMinX() 
							    - ( mtkScalingHelper.getScreenToMapFactor() * viewPort.getViewPosition().x	)
								) * mtkScalingHelper.getMapToScreenFactor() );
		int y =(int)Math.abs( ( Math.abs( mtkScalingHelper.getMaxY() ) - Math.abs( selectedShapesBounds.getMinY() )   
								- ( mtkScalingHelper.getScreenToMapFactor() * viewPort.getViewPosition().y	) )
								* mtkScalingHelper.getMapToScreenFactor() );
		// +1 because of rounding issues
		int w =(int)Math.abs( selectedShapesBounds.getWidth() * mtkScalingHelper.getMapToScreenFactor() ) + 1;
		int h =(int)Math.abs( selectedShapesBounds.getHeight() * mtkScalingHelper.getMapToScreenFactor() ) + 1;	
		
		logger.debug( "x=" + x + ", y=" + y + ", w=" + w + ", h=" + h );
		
		selectedShapesImg =viewImg.getSubimage( x, y, w, y + h < viewImg.getHeight() ? h : viewImg.getHeight() - y );

/*		// For debugging purpose only
	    try {
	    	File file = new File("d:/temp/selectedShapesImg.png");
	    	ImageIO.write( selectedShapesImg, "png", file);

    	} catch (IOException e) {
    		logger.error(e);
    	}*/
    	
	    g.dispose();
	}
	
	public void paintSelectedShapes( Graphics2D g ) {

		if ( getSelectedShapes().size() < 1 ) {
			return;
		}
		
		Rectangle2D selectedShapesBounds =null;
		
		for( MapShape shape : getSelectedShapes() ) {
			if ( selectedShapesBounds == null ) {
				selectedShapesBounds =(Rectangle2D)shape.getBounds().clone();
			} else {
				selectedShapesBounds.add( (Rectangle2D)shape.getBounds() );
			} 
		}
		
		JScrollPane sp =(JScrollPane)container.getParent().getParent();
		JViewport viewPort = sp.getViewport();
		
		Graphics2D g2d =(Graphics2D)sp.getGraphics();
		
		int x =(int)( ( selectedShapesBounds.getMinX() -mtkScalingHelper.getMinX() 
			    - ( mtkScalingHelper.getScreenToMapFactor() * viewPort.getViewPosition().x	)
				) * mtkScalingHelper.getMapToScreenFactor() );
		int y =(int)Math.abs( ( -mtkScalingHelper.getMaxY() - selectedShapesBounds.getMinY()    
				+ ( mtkScalingHelper.getScreenToMapFactor() * viewPort.getViewPosition().y	) )
				* mtkScalingHelper.getMapToScreenFactor() );
		
/*		logger.debug("y=" + y + "\nmap.minY=" + mtkScalingHelper.getMinY() + "\nmap.maxY=" +  mtkScalingHelper.getMaxY()
						+ "\nminY=" + selectedShapesBounds.getMinY() + "\nmaxY=" + selectedShapesBounds.getMaxY()
						+ "\nmapToScreenFactor=" + mtkScalingHelper.getMapToScreenFactor()  
						+ "\nviewPosition.y=" + viewPort.getViewPosition().y  );*/
					
		
	    // Main rendering loop. Volatile images may lose their contents. 
	    // This loop will continually render to (and produce if necessary) volatile images
	    // until the rendering was completed successfully.
		do {
			 
			if ( volatileImage == null ) {
				 createBackBuffer(); // recreate the hardware accelerated image.
			}
		   
		   // Validate the volatile image for the graphics configuration of this 
		   // component. If the volatile image doesn't apply for this graphics configuration 
		   // (in other words, the hardware acceleration doesn't apply for the new device)
		   // then we need to re-create it.
		   GraphicsConfiguration gc = container.getGraphicsConfiguration();
		   int valCode = volatileImage.validate(gc);
		   
		   // This means the device doesn't match up to this hardware accelerated image.
		   if ( valCode == VolatileImage.IMAGE_INCOMPATIBLE ) {
			   createBackBuffer(); // recreate the hardware accelerated image.
		   }
		  
		   Graphics offscreenGraphics = volatileImage.getGraphics();   
		   offscreenGraphics.drawImage( mtkBitmapHelper.getThematicMapBackground(), 0, 0, null );
			
		   offscreenGraphics.drawImage( selectedShapesImg, x, y, null );
		   
		   // paint back buffer to main graphics
		   g2d.drawImage(volatileImage, sp.getInsets().left, sp.getInsets().top, null);
		   // Test if content is lost   
		} while( volatileImage.contentsLost() );
		
		/*
		// for test purpose only
		try {
	        File file = new File("d:/temp/test4.png");
	        ImageIO.write( bi, "png", file);

	    } catch (IOException e) {
	    	logger.error(e);
	    }
		*/ 
	}
	
	private void createBackBuffer() {
		GraphicsConfiguration gc = container.getGraphicsConfiguration();
		
		JScrollPane sp =(JScrollPane)container.getParent().getParent();
		
		volatileImage = gc.createCompatibleVolatileImage( sp.getViewport().getWidth(), sp.getViewport().getHeight() );
	}
	
	private void resetBackBuffer() {
		volatileImage =null;
	}
	
	/**
	 * Draw map
	 */
	public void paintThematicMap( Graphics2D g, Map<String, Object> annotationContext, PageFormat pf ) {
		
		updateTitle();
		
		if ( isAnnotationsVisible() && container != null ) {
			container.setBackground( thematicMap.getAnnotations().getBackgroundColor() );
		}
		
		// If printing, process map factor for the printer page dimensions
		if ( pf != null ) {
			mtkScalingHelper.processMapFactor( pf.getImageableWidth(), pf.getImageableHeight() );
		}
		
		// Use antialiasing so it looks good especially when zooming
		setRenderingHint( g );
		
		Rectangle2D newBounds =new Rectangle2D.Double( 0, 0, 0, 0 );	
				
		mtkPaintHelper.paintBackgroundImage( g, bis, pf );
		
		// Apply zoom factor
		mtkScalingHelper.scale( g );
		
		// Translate based on minx & maxy
		translate( g, pf );
		
		mtkScalingHelper.setStrokeWidth( g );
	    
	    List<AnnotationShape> annShapes =new ArrayList<AnnotationShape>();
	    
	    mtkPaintHelper.paintMap( g, annShapes, newBounds );
		
		mtkPaintHelper.paintCD( g );
		
		mtkPaintHelper.paintAnnotations( g, annShapes, newBounds, annotationContext );	
		
		// Have bounds of the map changed ?		
		mtkScalingHelper.manageBounds( newBounds, container );
		
		resetBackBuffer();
		
		/*g.draw( getBounds() );
		
		g.draw( new Rectangle2D.Double( mtkScalingHelper.getMinX(), -mtkScalingHelper.getMaxY(), 
										Math.abs( mtkScalingHelper.getMaxX() - mtkScalingHelper.getMinX() ),
										Math.abs( mtkScalingHelper.getMaxY() - mtkScalingHelper.getMinY() ) ) );*/
	}
	
	/**
	 * Repaint
	 */
	public void repaint() {
		container.repaint();
	}
		
	/**
	 * 	If there are several shapes at this point (eg a polygon and its label)
	 *	get the smaller one
	 */
	public MapShape getShapeAt( Point2D pt ) {
		return mtkShapeHelper.getShapeAt( pt );
	}	
	
	public MapShape selectShapeAt( Point2D pt ) {
		return mtkShapeHelper.selectShapeAt( pt );
	}	
	
	public void selectShapes( List<MapShape> mapShapes ) {
		mtkShapeHelper.selectShapes( mapShapes );
	}
	
	public List<MapShape> getShapes() {
	    return shapes;
	}
	
	public List<MapShape> getSelectedShapes() {
	    return selectedShapes;
	}
	
	public MapShape[] getShapesAt( Point2D p1, Point2D p2 ) {
		return mtkShapeHelper.getShapesAt( p1, p2 );
	}	
	
	public void selectShapesAt( Point2D p1, Point2D p2 ) {
		mtkShapeHelper.selectShapesAt( p1, p2 );
	}
	
	public MapShape getHandleAt( Point2D p ) {
		return mtkShapeHelper.getHandleAt( p );
	}
	
	public MapShape addShape( int shape ) throws TMapException {
		return mtkShapeHelper.addShape( shape );
	}
	
	public void addShapes( List<MapShape> shapeList ) throws TMapException {
	    mtkShapeHelper.addShapes( shapeList );
	}
	
	public void removeShape( MapShape shape ) {
		mtkShapeHelper.removeShape( shape );
	}
	
	public void unselectShapes() {
		mtkShapeHelper.unselectShapes();
	}
	
	public void moveSelectedShapes( Point2D draggingPt ) {
		mtkShapeHelper.moveSelectedShapes( draggingPt );
	}
	
	/*
	 *  Returns true if a CDPointSymbol was deleted
	 */
	public boolean deleteSelectedShapes() {
		return mtkShapeHelper.deleteSelectedShapes();
	}
	
	public MapShape mergeSelectedPolygons() throws TMapException {
		return mtkShapeHelper.mergeSelectedPolygons();
	}
	
	public List<MapShape> splitPolygon( PolygonShape polygon, Point2D firstPt, Point2D secondPt ) throws TMapException {
		return mtkShapeHelper.splitPolygon( polygon, firstPt, secondPt );
	}
	
	public void addDisputedBorder( Point2D pt ) {
		mtkShapeHelper.addDisputedBorder( pt );		
	}
	
	public boolean anyShapeSelected() {
		return mtkShapeHelper.anyShapeSelected();
	}
	
	public void closePolygon() {
		mtkShapeHelper.closePolygon();
	}	
	
	public Map<String, PolygonShape> getOrphanPolygons() throws TMapException {
		return mtkShapeHelper.getOrphanPolygons();
	}
	
	public List<String> getNonMatchingData() {
		return mtkShapeHelper.getNonMatchingData();
	}
	
	public Map<String,PolygonShape> getPolygonsForSelectionByName() {
		return mtkShapeHelper.getPolygonsForSelectionByName();
	}
	
	public void selectPolygonsSelectedByName( List<String> selectedPolygons ) {
		mtkShapeHelper.selectPolygonsSelectedByName( selectedPolygons );
	}
	
	public BaseMap retrieveBaseMap() throws TMapException {
		return mtkShapeHelper.retrieveBaseMap();
	}	
      
    public void fitToWindow() {
    	mtkScalingHelper.fitToWindow( container );
    }
    
    public void zoomIn( Point2D pt ) {
    	mtkScalingHelper.zoomIn( pt, container );
    }
    
    public void zoomOut( Point2D pt ) {
    	mtkScalingHelper.zoomOut( pt, container );
    }    
    
	public void updateGeographicBox( double xmin, double xmax, double ymin, double ymax ) {
		mtkScalingHelper.updateGeographicBox( xmin, xmax, ymin, ymax );
	}
	
	public void setDefaultGeographicBox() {
		mtkScalingHelper.setDefaultGeographicBox();
	}
    
	public Point2D convertScreenToMap( Point2D pt ) {
		return mtkScalingHelper.convertScreenToMap( pt );
	}
	
	public Point2D convertMapToScreen( Point2D pt ) {
		return mtkScalingHelper.convertMapToScreen( pt );
	}
	
	public void computeDigitizerFactors( double xMinD, double yMinD, double xMaxD, double yMaxD ) {
		mtkScalingHelper.computeDigitizerFactors( xMinD, yMinD, xMaxD, yMaxD );
	}
	
	public Point2D convertDigitizerToMap( Point2D pt ) {
		return mtkScalingHelper.convertDigitizerToMap( pt );
	}

    /**
     * @param container The container to set.
     */
    public void setContainer(Container container) {
        this.container = container;
    }
    
    public Container getContainer() {
    	return container;
    }
    
    public Rectangle2D getBounds() {
        return mtkScalingHelper.getBounds();
    }
    
    public boolean boundsHaveChanged() {
        return mtkScalingHelper.isBoundsHaveChanged();
    }
    
    /**
     * @return Returns the maxX.
     */
    public double getMaxX() {
        return mtkScalingHelper.getMaxX();
    }
    
    /**
     * @param maxX The maxX to set.
     */
    public void setMaxX(double maxX) {
    	mtkScalingHelper.setMaxX( maxX );
    }
    /**
     * @return Returns the maxY.
     */
    public double getMaxY() {
        return mtkScalingHelper.getMaxY();
    }
    
    /**
     * @param maxY The maxY to set.
     */
    public void setMaxY(double maxY) {
    	mtkScalingHelper.setMaxY( maxY );
    }
    
    /**
     * @return Returns the minX.
     */
    public double getMinX() {
        return mtkScalingHelper.getMinX();
    }
    
    /**
     * @param minX The minX to set.
     */
    public void setMinX(double minX) {
    	mtkScalingHelper.setMinX( minX );
    }
    
    /**
     * @return Returns the minY.
     */
    public double getMinY() {
        return mtkScalingHelper.getMinY();
    }
    
    /**
     * @param minY The minY to set.
     */
    public void setMinY(double minY) {
    	mtkScalingHelper.setMinY( minY );
    }	    

	/**
	 * @return
	 */
	public Point2D getAnchor() {
		return anchor;
	}

	/**
	 * @return
	 */
	public Point2D getDrag() {
		return drag;
	}

	/**
	 * @param point
	 */
	public void setAnchor(Point2D point) {
		anchor = point;
	}

	/**
	 * @param point
	 */
	public void setDrag(Point2D point) {
		drag = point;
	}

	/**
	 * @return
	 */
	public MapShape getEditedShape() {
		return editedShape;
	}
	
	public void setEditedShape(MapShape shape) {
		editedShape =shape;
	}	

	/**
	 * @return
	 */
	public Color getCurrentColor() {
		return currentColor;
	}

	/**
	 * @param color
	 */
	public void setCurrentColor(Color color) {
		currentColor = color;
	}
	
    public void setBackgroundColor(Color color) {
        backgroundColor =color; 
    }
    
    public Color getBackgroundColor() {
        return backgroundColor;
    }

    /**
     * @return Returns the zoomFactor.
     */
    public int getZoomFactor() {
        return mtkScalingHelper.getZoomFactor();
    }    
    
    public TitleShape getTitleShape() {
		return titleShape;
	}
    

	public void setTitleShape(TitleShape titleShape) {
		this.titleShape = titleShape;
	}
	

	public LegendShape getLegendShape() {
		return legendShape;
	}
	

	public void setLegendShape(LegendShape legendShape) {
		this.legendShape = legendShape;
	}
	

	/**
     * @param zoomFactor The zoomFactor to set.
     */
    public void setZoomFactor(int zoomFactor) {
    	mtkScalingHelper.setZoomFactor( zoomFactor, container );
    }    

	public boolean saveThematicMapAsBitmap( File file, String extension, int width, int height, boolean removeBlankEdges ) throws ImageFormatException, IOException {
    	
    	String fileName =file.getAbsolutePath();
    	if ( file.getName().indexOf( '.' ) != -1 ) {
	    	int pos =fileName.lastIndexOf('.');
	    	if ( pos != -1 ) {
	    		fileName =fileName.substring( 0, pos );
	    	}
    	}
    	
    	fileName =fileName + "." + extension;
    	
    	boolean success =false;
    	
    	try {
    		BufferedImage img =retrieveThematicMapImage( width, height );
    		
    		if ( removeBlankEdges ) {
    			img =removeBlankEdges( img );
    		}
    	
    		success =ImageIO.write( img, extension, new File( fileName ) );
    	} catch( Exception e ) {
    		logger.error( e );
    	}

    	return success;
    }
    
    /**
     * Build an image (a bitmap) of the thematic map
     * @return the thematic map as a buffered image
     */ 
    private BufferedImage retrieveThematicMapImage() {
	    RepaintManager currentManager = RepaintManager.currentManager(container);
	    currentManager.setDoubleBufferingEnabled(false);        
    
		BufferedImage image = new BufferedImage( container.getWidth(), container.getHeight(), BufferedImage.TYPE_INT_RGB );
		
		Graphics2D g2 = image.createGraphics();

		container.paint(g2);
		
		g2.dispose();
		
		currentManager = RepaintManager.currentManager(container);		
	    currentManager.setDoubleBufferingEnabled(true);
	    
	    return image;
    }
    
    public BufferedImage retrieveThematicMapImage( int width, int height ) {
    	return retrieveThematicMapImage( width, height, null );
    }
    
    /**
     * Build an image (a bitmap) of the thematic map
     * @return the thematic map as a buffered image
     */    
    public BufferedImage retrieveThematicMapImage( int width, int height, Map<String, Object> annotationContext ) {
		BufferedImage bi = new BufferedImage( width, height, BufferedImage.TYPE_INT_RGB );
	    Graphics2D g2bi =(Graphics2D)bi.createGraphics();

	    g2bi.setBackground( thematicMap.getAnnotations().getBackgroundColor() );
	    g2bi.setColor( thematicMap.getAnnotations().getBackgroundColor() );	    

	    Rectangle rect =new Rectangle( new Dimension( bi.getWidth(), bi.getHeight() ) );

	    g2bi.draw( rect );
	    g2bi.fill( rect );
	    
	    //g2bi.setBackground( Color.WHITE );
	    g2bi.setColor( Color.BLACK );	    
	    
	    mtkScalingHelper.processMapFactor( (double)bi.getWidth(), (double)bi.getHeight() );
	    
	    paintThematicMap( g2bi, annotationContext, null );
	    
	    processMapFactor();
	    
	    g2bi.dispose();
	    
	    return bi;
    }
    
	/**
	 * Copy the selected shapes to the clipboard
	 * see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4498396 for issues with Linux
	 *
	 */    
    public void clipboardCopy() throws Exception {       
		Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
		
		BufferedImage img =removeBlankEdges( retrieveThematicMapImage() );
		
		clipboard.setContents( new ClipboardImage( img ), this );
    }    
    
    /**
     * Remove blank edges (on the right and on the bottom) 
     * around thematic map bitmap
     * @param img
     * @return
     */
    private BufferedImage removeBlankEdges( BufferedImage img ) throws Exception {
    	Dimension dim =mtkScalingHelper.getDimForEdgeRemoval( img.getWidth(), img.getHeight() );
    	
    	BufferedImage fixedImg =dim.width > img.getWidth() || dim.height > img.getHeight() 
    								? img
    								: img.getSubimage( 0, 0, dim.width, dim.height );
    	
    	return fixedImg;
    }

    /**
     * ClipboardOwner interface
     * Necessary for clipboard copy operation under some OS
     */
    public void lostOwnership(Clipboard clipboard, Transferable contents) {
        // Nothing to do for MS Windows
    }    
    
    /**
     * @see java.awt.print.Printable interface
     */
	public int print( Graphics g, PageFormat pf, int pageIndex ) {
		if (pageIndex == 0) {			
			paintThematicMap( (Graphics2D)g, null, pf );
			
			return Printable.PAGE_EXISTS;					
		} else {
			return Printable.NO_SUCH_PAGE;
		}
	}    
    
	public boolean isAnnotationsVisible() {
		return annotationsVisible;
	}

	public void setAnnotationsVisible(boolean annotationsVisible) {
		this.annotationsVisible = annotationsVisible;
	}

	public boolean isMapVisible() {
		return baseMapVisible;
	}

	public void setMapVisible(boolean baseMapVisible) {
		this.baseMapVisible = baseMapVisible;
	}

	public boolean isCartographicDisplayVisible() {
		return cartographicDisplayVisible;
	}

	public void setCartographicDisplayVisible(boolean cartographicDisplayVisible) {
		this.cartographicDisplayVisible = cartographicDisplayVisible;
	}

	public ThematicMap getThematicMap() {
		return thematicMap;
	}

	public void setThematicMap(ThematicMap thematicMap) {
		this.thematicMap = thematicMap;
	}	

    public int getEditMode() {
		return editMode;
	}

	public void setEditMode(int editMode) {
		this.editMode = editMode;
	}

	@SuppressWarnings("unchecked")
	public List addPointEntry( Point2D pt, String description ) {
		return mtkShapeHelper.addPointEntry( pt, description );
	}
	
    public void selectCDPointsByDesc( List<Point2D> cdPointsDesc ) {
    	mtkShapeHelper.selectCDPointsByDesc( cdPointsDesc );
    	container.repaint();
    }
    
    public String getVersion() {
    	return VERSION;
    }
    
    public BackgroundImageShape getBackgroundImage() {
    	return bis;
    }
    
    public void setBackgroundImage( BackgroundImageShape bis ) {
    	this.bis.assignValues( bis );
    	
    	if ( getMinX() == 0 && getMaxX() == 0 && getMinY() == 0 && getMaxY() == 0 && container != null ) {

    		double w, h;
    		if ( container.getParent().getParent() instanceof JScrollPane ) {
    			JScrollPane scrollPane =(JScrollPane)container.getParent().getParent();
    			JViewport viewPort =scrollPane.getViewport();
    			
                w =(int)viewPort.getSize().getWidth() - (int)scrollPane.getVerticalScrollBar().getWidth();
                h =(int)viewPort.getSize().getHeight() - (int)scrollPane.getHorizontalScrollBar().getHeight(); 
    		} else {
    			w =container.getWidth();
    			h =container.getHeight();
    		}
    		
    		logger.debug( "setBackgroundImage: container is " + container.getClass().getName() );
    		logger.debug( "w=" + w + ", h=" + h );
           
    		int width =(int)(w * getMapFactor() * ( (double)getZoomFactor() / 100d ));
    		double ratio =bis.getImageHeight() != 0 ? (double)bis.getImageWidth() / (double)bis.getImageHeight()
    										 		: 1.d;	
    		int height=(int)((double)width / ratio);
    		
    		updateGeographicBox(0, (double)width, 0, (double)height );
    	}
    }
    
    public void initializeGeoBoxUsingBackgroundImage() {
    	mtkScalingHelper.initializeGeoBoxUsingBackgroundImage( container, bis );
    }
    
    public void selectShape( MapShape mapShape ) {
    	mtkShapeHelper.selectShape( mapShape );
    }

	public int getDbaseJoinIdx() {
		return dbaseJoinIdx;
	}

	public void setDbaseJoinIdx(int dbaseJoinIdx) {
		this.dbaseJoinIdx = dbaseJoinIdx;
	}    

	public void captureThematicMapBackground() {
		mtkBitmapHelper.captureThematicMapBackground( container );
	}
	
	public void displayThematicMapBackground() {
		mtkBitmapHelper.displayThematicMapBackground( container );
	}
	
	public void setSelectedShapesHidden( boolean selectedShapesHidden ) {
		mtkPaintHelper.setSelectedShapesHidden( selectedShapesHidden );
	}
	
	public boolean isSelectedShapesHidden() {
		return mtkPaintHelper.isSelectedShapesHidden();
	}
	
	public void projectShapes() {

		mtkPaintHelper.projectShapes(container);
				
		processMapFactor();
		mtkScalingHelper.revalidateScrollPane( container );
		repaint();
	}
	
}
